/*
 * Amaze - QT downloader for the Amazon MP3 store
 * Copyright (c) 2010-2011 Robert Keevil
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * Information about the .amz file format, its encoding and
 * cryptographic details (keys, ciphers, blocks), was taken from:
 *
 * clamz - Command-line downloader for the Amazon.com MP3 store
 * Copyright (c) 2008-2010 Benjamin Moody
 */

#include "parse.h"

extern "C" {
#include "des.h"
}

#ifdef _MSC_VER
#pragma warning(disable:4309)
#endif

Parse::Parse(Amaze *parent)
{
    amaze = parent;
}

/*
  A simple, crude CBC implementation
  */
bool Parse::decrypt(QByteArray input, QString &output)
{
#define CIPHER_BLOCK_SIZE 8
    output.clear();
    char key[CIPHER_BLOCK_SIZE] = { 0x29, 0xAB, 0x9D, 0x18, 0xB2, 0x44, 0x9E, 0x31 };
    char Ci_1[CIPHER_BLOCK_SIZE] = { 0x5E, 0x72, 0xD7, 0x9A, 0x11, 0xB3, 0x4F, 0xEE }; // IV
    const char NULL_BYTE = '\0';
    gl_des_ctx context;

    // Set up the DES encryption context
    gl_des_setkey(&context, key);

    if (input.isEmpty()) return false;

    // round up to factor of block size
    while (input.length() % CIPHER_BLOCK_SIZE)
    {
        input.append(NULL_BYTE);
    }

    // allocate output memory
    output.reserve(input.length());

    // loop through blocks
    for (int i = 0; i < input.length(); i += CIPHER_BLOCK_SIZE)
    {
        char Pi[CIPHER_BLOCK_SIZE], Ci[CIPHER_BLOCK_SIZE];

        // current block
        char *chunk = input.mid(i, CIPHER_BLOCK_SIZE).data();
        for (int l = 0; l < CIPHER_BLOCK_SIZE; l++)
            Ci[l] = chunk[l];

        // decrypt (to ECB) current block
        gl_des_ecb_decrypt(&context, Ci, Pi);
        // XOR against Ci
        for (int k = 0; k < CIPHER_BLOCK_SIZE; k++)
        {
            Pi[k] ^= Ci_1[k];
            output.append(Pi[k]);
        }

        // set Ci for next block
        for (int j = 0; j < CIPHER_BLOCK_SIZE; j++)
            Ci_1[j] = Ci[j];
    }

    return true;
}

bool Parse::open(QFile *target)
{
    emit add_log(LOG_DEBUG, QString("Attempting to parse %1")
                 .arg(target->fileName()));
    amaze->tracks->clear();

    target->open(QIODevice::ReadOnly);
    if (!target->isOpen())
    {
        emit add_log(LOG_ERROR, tr("Unable to open %1 for reading")
                     .arg(target->fileName()));
        return false;
    }

    QByteArray data;
    data = target->readAll();
    target->close();

    if (data.length() == 0)
    {
        // empty file, or error during readAll()
        emit add_log(LOG_ERROR, tr("%1 is empty, or error during read")
                     .arg(target->fileName()));
        return false;
    }

    /*
     * Check the magic at the start of all .amz files
     */
    if (data.startsWith("gtBcZ7b78K") || data.startsWith("nC4QN2vTDf"))
    {
        emit add_log(LOG_DEBUG, "Valid header found");
    }
    else
    {
        emit add_log(LOG_ERROR,
                     tr("This does not appear to be a valid .amz file"));
        return false;
    }

    QByteArray decoded = QByteArray::fromBase64(data);
    QString decrypted;

    if(!decrypt(decoded, decrypted))
    {
        emit add_log(LOG_ERROR, tr("Error decrypting .amz file"));
        return false;
    }

    // trim junk after the last tag.
    decrypted.truncate(decrypted.lastIndexOf('>') + 1);

    emit add_log(LOG_TRACE, decrypted);

    /*
     * Sanity check to ensure this is an XML file
     */
    if (data.startsWith("gtBcZ7b78K") && !decrypted.startsWith("<?xml"))
    {
        emit add_log(LOG_ERROR, tr(".amz file did not contain an XML file"));
        return false;
    }

    reader = new QXmlStreamReader(decrypted);

    reader->readNext();
    if (reader->atEnd())
    {
        emit add_log(LOG_ERROR, tr("Empty XML file"));
        return false;
    }

    while (!reader->atEnd())
    {
        if (reader->isStartElement())
        {
            //emit add_log(LOG_TRACE, reader->name().toString());
            if (reader->name() == "track")
            {
                readtrack();
            }
        }

        reader->readNext();
    }
    if (reader->hasError())
    {
        emit add_log(LOG_INFO, tr("XML Error: %1").arg(reader->errorString()));
    }

    emit add_log(LOG_DEBUG, "Successfully parsed file");
    emit parsed();
    return true;
}

void Parse::readtrack()
{
    track tmp;
    tmp.filesize = 0;
    tmp.state = TRACK_NEW;
    reader->readNext();
    while (!reader->atEnd())
    {
        if (reader->isStartElement())
        {
            //qDebug() << reader->name();
            if (reader->name() == AMZ_LOCATION)
                tmp.url = reader->readElementText();
            else if (reader->name() == AMZ_ARTIST)
                tmp.artist = QString::fromUtf8(
					reader->readElementText().toAscii());
            else if (reader->name() == AMZ_ALBUM)
                tmp.album = QString::fromUtf8(
					reader->readElementText().toAscii());
            else if (reader->name() == AMZ_TITLE)
                tmp.title = QString::fromUtf8(
					reader->readElementText().toAscii());
            else if (reader->name() == AMZ_IMAGE)
                tmp.image_url = reader->readElementText();
            else if (reader->name() == AMZ_LENGTH)
                tmp.duration = reader->readElementText();
            else if (reader->name() == AMZ_TRACKNUM)
                tmp.tracknum = reader->readElementText();
            else if (reader->name() == "meta")
            {
                // now read the meta rel= data
                QXmlStreamAttributes attributes = reader->attributes();
                for (int i = 0; i < attributes.size(); i++)
                {
                    QString value = attributes.at(i).value().toString();
                    if (value.contains(AMZ_META_ASIN))
                        tmp.asin = reader->readElementText();
                    else if (value.contains(AMZ_META_PRODUCTTYPE))
                        tmp.product_type = reader->readElementText();
                    else if (value.contains(AMZ_META_GENRE))
                        tmp.genre = QString::fromUtf8(
							reader->readElementText().toAscii());
                    else if (value.contains(AMZ_META_FILESIZE))
                        tmp.filesize = reader->readElementText().toInt();
                    else if (value.contains(AMZ_META_DISCNUM))
                        tmp.discnum = reader->readElementText();
                    else if (value.contains(AMZ_META_ALBUMASIN))
                        tmp.album_asin = reader->readElementText();
                    else if (value.contains(AMZ_META_ALBUMARTIST))
                        tmp.album_artist = QString::fromUtf8(
							reader->readElementText().toAscii());
                    else if (value.contains(AMZ_META_TRACKTYPE))
                        tmp.track_type = QString::fromUtf8(
							reader->readElementText().toAscii());
                }
            }
        }
        else if (reader->isEndElement())
        {
            if (reader->name() == "track")
            {
                amaze->tracks->append(tmp);
                return;
            }
        }

        reader->readNext();
    }
}
